/*
 * Copyright (c) 2021, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.ohos.camera;

import boofcv.ohos.HeLog;
import ohos.aafwk.ability.AbilitySlice;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.surfaceprovider.SurfaceProvider;
import ohos.agp.graphics.Surface;
import ohos.agp.graphics.SurfaceOps;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.media.camera.CameraKit;
import ohos.media.camera.device.Camera;
import ohos.media.camera.device.CameraAbility;
import ohos.media.camera.device.CameraConfig;
import ohos.media.camera.device.CameraInfo;
import ohos.media.camera.device.CameraStateCallback;
import ohos.media.camera.device.FrameConfig;
import ohos.media.camera.params.Metadata;
import ohos.media.camera.params.PropertyKey;
import ohos.media.image.Image;
import ohos.media.image.ImageReceiver;
import ohos.media.image.common.ImageFormat;
import ohos.media.image.common.Size;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import static ohos.media.camera.device.Camera.FrameConfigType.FRAME_CONFIG_PREVIEW;

/**
 * SimpleCameraSlice
 *
 * @author:Peter Abeles
 * @since 2021-05-11
 */
public abstract class SimpleCameraSlice extends AbilitySlice {
    private static final String TAG = "SimpleCameraSlice";
    protected CameraOpen cameraOpen = new CameraOpen();
    protected int viewWidth; // width and height of the view the camera is displayed in
    protected int viewHeight;
    protected boolean isVerbose = false; // If true there will be verbose output to Log
    private SurfaceProvider mSurfaceProvider;
    private Component drawComponent;
    private final CameraStateCallback mCameraStateCallback = new CameraStateCallback() {
        /**
         * onCreated
         * @param camera
         */
        @Override
        public void onCreated(Camera camera) {
            if (isVerbose) {
                HeLog.i(TAG, "CameraStateCallback onCreated");
            }
            cameraOpen.mCamera = camera;
            CameraConfig.Builder cameraConfigBuilder = camera.getCameraConfigBuilder();
            cameraConfigBuilder.addSurface(cameraOpen.mImageReceiver.getRecevingSurface());
            if (mSurfaceProvider != null) {
                cameraOpen.surface = mSurfaceProvider.getSurfaceOps().get().getSurface();
                cameraConfigBuilder.addSurface(cameraOpen.surface);
            }
            camera.configure(cameraConfigBuilder.build());
        }

        /**
         * onConfigured
         * @param camera
         */
        @Override
        public void onConfigured(Camera camera) {
            if (isVerbose) {
                HeLog.i(TAG, "CameraStateCallback onConfigured");
            }
            cameraOpen.mFrameConfigBuilder = cameraOpen.mCamera.getFrameConfigBuilder(FRAME_CONFIG_PREVIEW);
            cameraOpen.mFrameConfigBuilder.addSurface(cameraOpen.mImageReceiver.getRecevingSurface());
            if (mSurfaceProvider != null) {
                cameraOpen.mFrameConfigBuilder.addSurface(cameraOpen.surface);
            }
            configureCamera(cameraOpen.mCamera, cameraOpen.mCameraAbility, cameraOpen.mFrameConfigBuilder);
            try {
                // 启动循环帧捕获
                camera.triggerLoopingCapture(cameraOpen.mFrameConfigBuilder.build());
            } catch (IllegalArgumentException e) {
                HeLog.d("Argument Exception");
            } catch (IllegalStateException e) {
                HeLog.d("State Exception");
            }
        }
    };

    private ImageReceiver.IImageArrivalListener onAvailableListener = imageReceiver -> {
        if (isVerbose) {
            HeLog.i(TAG, "IImageArrivalListener onImageArrival");
        }
        if (imageReceiver.getCapacity() == 0) {
            HeLog.d("No images available. Has image.close() not been called?");
            return;
        }

        Image image = imageReceiver.readLatestImage();
        if (image == null) {
            HeLog.d("OnImageAvailableListener: acquireLatestImage() returned null");
            return;
        }

        processFrame(image, imageReceiver.getImageSize().width, imageReceiver.getImageSize().height);
        image.release();
    };

    /**
     * 日志打印类
     *
     * @author:zhaojun
     * @since 2021-05-11
     */
    static class CameraOpen {
        ReentrantLock mLock = new ReentrantLock();
        CameraState state = CameraState.CLOSED;
        Camera mCamera;
        Surface surface;
        Size mCameraSize; // size of camera preview
        String cameraId; // the camera that was selected to view
        int mSensorOrientation; // sensor's orientation
        CameraAbility mCameraAbility; // describes physical properties of the camera
        private ImageReceiver mImageReceiver; // Image reader for capturing the preview
        private FrameConfig.Builder mFrameConfigBuilder;

        /**
         * closeCamera
         */
        public void closeCamera() {
            state = CameraState.CLOSED;
            mCamera.release();
            mImageReceiver.release();
            clearCamera();
        }

        /**
         * clearCamera
         *
         * @throws RuntimeException
         */
        public void clearCamera() {
            if (!mLock.isLocked()) {
                throw new RuntimeException("Calling clearCamera() when not locked!");
            }
            mCameraAbility = null;
            mCamera = null;
            mCameraSize = null;
            cameraId = null;
            mSensorOrientation = 0;
            mImageReceiver = null;
            mFrameConfigBuilder = null;
        }
    }

    /**
     * CameraState
     *
     * @author:zhaojun
     * @since 2021-05-11
     */
    protected enum CameraState {
        CLOSED,
        /**
         * The camera enters into this state the second an request is made to open the camera. At this point
         * none of the device isn't known.
         */
        OPENING,
        OPEN,
        /**
         * When in the closing state that means the camera was in the opening state when an close request was
         * sent. At various points in the opening process it should see if its in this state. If the camera
         * device is not null then the camera should be shut down. If null then just set the state to closed.
         */
        CLOSING
    }

    /**
     * Custom view for visualizing results
     *
     * @since 2021-05-11
     */
    public class SimpleSurfaceProvider extends SurfaceProvider implements SurfaceOps.Callback {
        /**
         * 构造方法
         *
         * @param context
         */
        public SimpleSurfaceProvider(Context context) {
            super(context);
            getSurfaceOps().get().addCallback(this);
        }

        /**
         * surfaceCreated
         *
         * @param surfaceOps
         */
        @Override
        public void surfaceCreated(SurfaceOps surfaceOps) {
            createCamera(surfaceOps.getSurfaceDimension().getWidth(), surfaceOps.getSurfaceDimension().getHeight());
        }

        /**
         * surfaceChanged
         *
         * @param surfaceOps
         * @param i
         * @param i1
         * @param i2
         */
        @Override
        public void surfaceChanged(SurfaceOps surfaceOps, int i, int i1, int i2) {
        }

        /**
         * surfaceDestroyed
         *
         * @param surfaceOps
         */
        @Override
        public void surfaceDestroyed(SurfaceOps surfaceOps) {
        }
    }

    /**
     * onActive
     */
    @Override
    protected void onActive() {
        super.onActive();
        if (cameraOpen.state == CameraState.CLOSING) {
            createCamera(viewWidth, viewHeight);
        }
    }

    /**
     * onInactive
     */
    @Override
    protected void onInactive() {
        super.onInactive();
        if (mSurfaceProvider != null) {
            closeCamera();
        } else {
            cameraOpen.state = CameraState.CLOSING;
        }
    }

    /**
     * onStop
     */
    @Override
    protected void onStop() {
        if (isVerbose) {
            HeLog.i(TAG, "onStop()");
        }
        super.onStop();

        closeCamera();
        if (mSurfaceProvider != null) {
            mSurfaceProvider.clearFocus();
            mSurfaceProvider.release();
            mSurfaceProvider = null;
        }
        if (drawComponent != null) {
            drawComponent.clearFocus();
            drawComponent.release();
            drawComponent = null;
        }
    }

    // ---------------自定义方法

    /**
     * 开始预览
     *
     * @param layout
     */
    protected void startCameraTexture(ComponentContainer layout) {
        startCameraTexture(layout, new SimpleSurfaceProvider(getContext()));
    }

    /**
     * 开始预览
     *
     * @param layout
     * @param simpleSurfaceProvider
     */
    protected void startCameraTexture(ComponentContainer layout, SimpleSurfaceProvider simpleSurfaceProvider) {
        if (isVerbose) {
            HeLog.i(TAG, "startCameraTexture");
        }
        if (layout == null) {
            return;
        }
        if (simpleSurfaceProvider == null) {
            return;
        }
        mSurfaceProvider = simpleSurfaceProvider;
        drawComponent = null;

        DirectionalLayout.LayoutConfig params = new DirectionalLayout.LayoutConfig(
                ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT);
        mSurfaceProvider.setLayoutConfig(params);
        mSurfaceProvider.pinToZTop(true);
        layout.addComponent(mSurfaceProvider);
    }

    /**
     * 开始预览
     *
     * @param layout
     * @param comt
     */
    protected void startCameraView(ComponentContainer layout, Component comt) {
        if (isVerbose) {
            HeLog.i(TAG, "startCameraView");
        }
        if (layout == null) {
            return;
        }
        if (drawComponent == null) {
            drawComponent = new Component(getContext());
        }
        mSurfaceProvider = null;
        drawComponent = comt;

        DirectionalLayout.LayoutConfig params = new DirectionalLayout.LayoutConfig(
                ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT);
        drawComponent.setLayoutConfig(params);
        layout.addComponent(drawComponent);

        drawComponent.setLayoutRefreshedListener(new Component.LayoutRefreshedListener() {
            @Override
            public void onRefreshed(Component component) {
                int width = component.getWidth();
                int height = component.getHeight();
                component.setLayoutRefreshedListener(null);
                createCamera(width, height);
            }
        });
    }

    /**
     * 创建camera
     *
     * @param width
     * @param height
     */
    protected void createCamera(int width, int height) {
        if (isVerbose) {
            HeLog.i(TAG, "createCamera width=" + width + " height=" + height);
        }
        CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
        String[] cameraIds = cameraKit.getCameraIds();
        final int capacity = 2;
        for (String cameraId : cameraIds) {
            CameraInfo characteristics = cameraKit.getCameraInfo(cameraId);
            if (!selectCamera(cameraId, characteristics)) {
                continue;
            }

            CameraAbility ability = cameraKit.getCameraAbility(cameraId);
            if (ability == null) {
                continue;
            }

            List<Size> sizes = ability.getSupportedSizes(ImageFormat.YUV420_888);
            int which = selectResolution(width, height, sizes);
            if (which < 0 || which >= sizes.size()) {
                continue;
            }

            if (mSurfaceProvider != null) {
                double ratio = Math.min(1.0 * width / sizes.get(which).width, 1.0 * height / sizes.get(which).height);
                DirectionalLayout.LayoutConfig params = new DirectionalLayout.LayoutConfig(
                        (int) (sizes.get(which).width * ratio), (int) (sizes.get(which).height * ratio));
                mSurfaceProvider.setLayoutConfig(params);
            }

            viewWidth = width;
            viewHeight = height;
            cameraOpen.mCameraSize = sizes.get(which);
            cameraOpen.mCameraAbility = ability;
            cameraOpen.cameraId = cameraId;
            cameraOpen.mSensorOrientation = (int) cameraOpen.mCameraAbility
                    .getPropertyValue(new PropertyKey.Key("ohos.camera.sensorOrientation", Integer.class));
            cameraOpen.mImageReceiver = ImageReceiver.create(
                    cameraOpen.mCameraSize.width, cameraOpen.mCameraSize.height,
                    ImageFormat.YUV420_888, capacity);
            cameraOpen.mImageReceiver.setImageArrivalListener(onAvailableListener);

            onCameraResolutionChange(cameraOpen.mCameraSize.width, cameraOpen.mCameraSize.height,
                    cameraOpen.mSensorOrientation);
//            configureTransform(width, height);
            cameraKit.createCamera(cameraOpen.cameraId, mCameraStateCallback,
                    new EventHandler(EventRunner.create("CameraBackground")));
            break;
        }
    }

    /**
     * By default this will select the backfacing camera. override to change the camera it selects.
     *
     * @param id
     * @param characteristics
     * @return boolean
     */
    protected boolean selectCamera(String id, CameraInfo characteristics) {
        if (isVerbose) {
            HeLog.i(TAG, "selectCamera id=" + id);
        }
        return characteristics.getFacingType() != CameraInfo.FacingType.CAMERA_FACING_FRONT;
    }

    /**
     * Selects the camera resolution from the list of possible values. By default it picks the
     * resolution which best fits the texture's aspect ratio. If there's an tie the area is
     * maximized.
     *
     * @param widthTexture Width of the texture the preview is displayed inside of.<=0 if no view
     * @param heightTexture Height of the texture the preview is displayed inside of.<=0 if no view
     * @param resolutions array of possible resolutions
     * @return index of the resolution
     */
    protected int selectResolution(int widthTexture, int heightTexture, List<Size> resolutions) {
        if (isVerbose) {
            HeLog.i(TAG, "selectResolution widthTexture=" + widthTexture + " heightTexture=" + heightTexture);
        }
        final int initBestIndex = -1;
        int bestIndex = initBestIndex;
        double bestAspect = Double.MAX_VALUE;
        double bestArea = 0;

        double textureAspect = widthTexture > 0 ? widthTexture / (double) heightTexture : 0;
        final double maxNum = 1e-8;

        for (int ii = 0; ii < resolutions.size(); ii++) {
            int width = resolutions.get(ii).width;
            int height = resolutions.get(ii).height;

            double aspectScore = widthTexture > 0 ? Math.abs(width - height * textureAspect) / width : 1;

            if (aspectScore < bestAspect) {
                bestIndex = ii;
                bestAspect = aspectScore;
                bestArea = width * height;
            } else if (Math.abs(aspectScore - bestArea) <= maxNum) {
                bestIndex = ii;
                double area = width * height;
                if (area > bestArea) {
                    bestArea = area;
                }
            }
        }

        return bestIndex;
    }

    /**
     * Process an single frame from the video feed. Image is automatically
     * closed after this function exists. No need to invoke image.close() manually.
     * <p>
     * All implementations of this function must run very fast. Less than 5 milliseconds is an good
     * rule of thumb. If longer than that then you should spawn an thread and process the
     * image inside of that.
     *
     * @param image
     * @param width
     * @param height
     */
    protected abstract void processFrame(Image image, int width, int height);

    /**
     * Override to do custom configuration of the camera's settings. By default the camera
     * is put into auto mode.
     *
     * @param device The camera being configured
     * @param characteristics Used to get information on the device
     * @param captureRequestBuilder used to configure the camera
     */
    protected void configureCamera(Camera device, CameraAbility characteristics,
                                   FrameConfig.Builder captureRequestBuilder) {
        if (isVerbose) {
            HeLog.i(TAG, "configureCamera");
        }
        captureRequestBuilder.setAfMode(Metadata.AfMode.AF_MODE_CONTINUOUS, null);
        captureRequestBuilder.setAeMode(Metadata.AeMode.AE_MODE_ON, null);
    }

    /**
     * Called when the camera's resolution has changed. This function can be called more than once
     * each time an camera is opened, e.g. requested resolution does not match actual.
     *
     * @param cameraWidth
     * @param cameraHeight
     * @param orientation
     */
    protected void onCameraResolutionChange(int cameraWidth, int cameraHeight, int orientation) {
        if (isVerbose) {
            HeLog.i(TAG, "onCameraResolutionChange");
        }
    }

    /**
     * Stops the capture session and allows you to reconfigure the camera, then starts it up again. You
     * reconfigure the camera when it calls the {@link #configureCamera}.
     *
     * @return true if the change was processed or rejected for some reason
     * @throws RuntimeException
     */
    protected boolean changeCameraConfiguration() {
        if (isVerbose) {
            HeLog.i(TAG, "changeCameraConfiguration()");
        }
        if (!EventRunner.getMainEventRunner().isCurrentRunnerThread()) {
            throw new RuntimeException("Not on main looper! Modify code to remove assumptions");
        }
        if (cameraOpen.mCamera == null || cameraOpen.mCameraSize == null) {
            HeLog.i(TAG, "  aborting changeCameraConfiguration. Camera not open yet.");
            return false;
        }
        return true;
    }

    /**
     * Closes the camera. Returns true if the camera was not already closed and it closed it
     *
     * @return boolean
     * @throws RuntimeException
     */
    protected boolean closeCamera() {
        if (isVerbose) {
            HeLog.i(TAG, "closeCamera()");
        }
        if (!EventRunner.getMainEventRunner().isCurrentRunnerThread()) {
            throw new RuntimeException("Attempted to close camera not on the main looper thread!");
        }
        boolean isClosed = false;
        /*
        NOTE: Since open can only be called in the main looper this won't be enough to prevent
        it from closing before it opens. That's why open.state exists
         */
        cameraOpen.mLock.lock();
        try {
            // close has been called while trying to open the camera!
            if (cameraOpen.state == CameraState.OPENING) {
                // If it's in this state that means an asych task is opening the camera. By changing the state
                // to closing it will not abort that process when the task is called.
                cameraOpen.state = CameraState.CLOSING;
                if (cameraOpen.mCamera != null) {
                    throw new RuntimeException("BUG! Camera is opening and should be null until opened");
                }
            } else {
                if (cameraOpen.mCamera != null) {
                    isClosed = true;
                    cameraOpen.closeCamera();
                }
                cameraOpen.state = CameraState.CLOSED;
                cameraOpen.clearCamera();
            }
        } finally {
            cameraOpen.mLock.unlock();
        }
        return isClosed;
    }
}
